Skip to content

feat: Add measured boot trust approvals#3464

Open
kfelternv wants to merge 11 commits into
NVIDIA:mainfrom
kfelternv:v21-measurables-rest
Open

feat: Add measured boot trust approvals#3464
kfelternv wants to merge 11 commits into
NVIDIA:mainfrom
kfelternv:v21-measurables-rest

Conversation

@kfelternv

@kfelternv kfelternv commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Adds Provider Admin REST operations for creating, listing, and deleting measured-boot trusted-machine and trusted-profile approvals. This exposes the existing Core capability through the established Temporal, SiteAgent, and Core gRPC path, including the OpenAPI contract and generated Go SDK.

Related issues

Part of #2801.

Type of Change

  • Add - New feature or capability
  • Change - Changes in existing functionality
  • Fix - Bug fixes
  • Remove - Removed features or deprecated functionality
  • Internal - Internal changes (refactoring, tests, docs, etc.)

Breaking Changes

  • This PR contains breaking changes

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No testing required (docs, internal refactor, etc.)

Additional Notes

The nicocli work in #2801 is not included in this PR.

Signed-off-by: Kyle Felter <kfelter@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds REST and Core support for creating, listing, and deleting measured-boot trusted machine and profile approvals, with validation, authorization, structured removal errors, routing, OpenAPI documentation, and automated tests.

Changes

Measurement trust approvals

Layer / File(s) Summary
Trust approval models and conversions
rest-api/api/pkg/api/model/measurementtrust.go, rest-api/api/pkg/api/model/measurementtrust_test.go
Defines approval models, validation, selector-based deletion requests, protobuf conversions, timestamp mapping, and model tests.
Trust approval handlers
rest-api/api/pkg/api/handler/measurementtrust.go
Adds authorized create, list, and delete handlers for machine and profile approvals, including tracing, Core calls, error handling, and response conversion.
Core removal semantics
crates/api-core/src/measured_boot/rpc/site.rs, crates/api-db/src/measured_boot/interface/site.rs
Uses TrustedMachineId for machine removal and maps missing approval records to structured not-found errors for machine and profile removal.
Routes and API specification
rest-api/api/pkg/api/routes.go, rest-api/api/pkg/api/routes_test.go, rest-api/openapi/spec.yaml
Registers six measured-boot approval routes and documents their operations, selectors, schemas, and error responses.
Integration and removal coverage
rest-api/api/pkg/api/handler/measurementtrust_test.go, crates/api-core/src/measured_boot/tests/rpc.rs
Tests handler wiring, proxied workflow requests, response mapping, validation, authorization, route registration, and Core removal behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant APIClient
  participant MeasurementTrustHandler
  participant CoreGrpcProxy
  participant MeasurementTrustRPC
  participant MeasurementTrustDB
  APIClient->>MeasurementTrustHandler: Create, list, or delete approval
  MeasurementTrustHandler->>MeasurementTrustHandler: Validate request and authorize site
  MeasurementTrustHandler->>CoreGrpcProxy: Execute measurement trust RPC
  CoreGrpcProxy->>MeasurementTrustRPC: Process approval operation
  MeasurementTrustRPC->>MeasurementTrustDB: Read or remove approval
  MeasurementTrustDB-->>MeasurementTrustRPC: Return record or database error
  MeasurementTrustRPC-->>CoreGrpcProxy: Return approval or structured error
  CoreGrpcProxy-->>MeasurementTrustHandler: Return protobuf response
  MeasurementTrustHandler-->>APIClient: Return HTTP response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the new measured-boot trust approval feature.
Description check ✅ Passed The description is clearly aligned with the added REST operations, OpenAPI updates, and testing changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@kfelternv
kfelternv requested a review from thossain-nv July 14, 2026 00:43
@kfelternv
kfelternv marked this pull request as ready for review July 14, 2026 01:38
@kfelternv
kfelternv requested a review from a team as a code owner July 14, 2026 01:38
@github-actions

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

🔍 Container Scan Summary

Service Total Critical High Medium Low Other
boot-artifacts-aarch64 3 0 0 3 0 0
boot-artifacts-x86_64 3 0 0 3 0 0
forge-admin-cli-x86_64 259 16 29 79 7 128
machine-validation-runner 538 41 117 184 15 181
machine_validation 538 41 117 184 15 181
machine_validation-aarch64 538 41 117 184 15 181
TOTAL 1879 139 380 637 52 671

Per-CVE detail lives in the per-service grype-* artifacts (JSON + SARIF). Severity counts only — no CVE IDs published here.

@github-actions

Copy link
Copy Markdown

🔐 TruffleHog Secret Scan

No secrets or credentials found!

Your code has been scanned for 700+ types of secrets and credentials. All clear! 🎉

🔗 View scan details

🕐 Last updated: 2026-07-14 01:42:37 UTC | Commit: cc4c71c

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc4c71c715

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rest-api/api/pkg/api/handler/measurementtrust.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
rest-api/api/pkg/api/model/measurementtrust.go (1)

17-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Model approval-type as a named type with receiver methods instead of raw string + free helpers.

approvalType is currently a bare string validated via a hand-rolled switch (validateMeasurementTrustApprovalType) and converted via free functions (measurementTrustApprovalTypeToProto/FromProto). Per coding guidelines, this should be a named type with receiver methods, and validation should prefer ozzo's built-in validation.In rule (or a func(interface{}) error validator usable with validation.By) rather than a bespoke switch.

♻️ Suggested refactor
-const (
-	MeasurementTrustApprovalTypeOneshot = "Oneshot"
-	MeasurementTrustApprovalTypePersist = "Persist"
-)
+type MeasurementTrustApprovalType string
+
+const (
+	MeasurementTrustApprovalTypeOneshot MeasurementTrustApprovalType = "Oneshot"
+	MeasurementTrustApprovalTypePersist MeasurementTrustApprovalType = "Persist"
+)
+
+func (t MeasurementTrustApprovalType) ToProto() corev1.MeasurementApprovedTypePb {
+	if t == MeasurementTrustApprovalTypePersist {
+		return corev1.MeasurementApprovedTypePb_Persist
+	}
+	return corev1.MeasurementApprovedTypePb_Oneshot
+}

Then use validation.Field(&r.ApprovalType, validation.Required, validation.In(MeasurementTrustApprovalTypeOneshot, MeasurementTrustApprovalTypePersist)) inside ValidateStruct.

Also applies to the wildcard MachineID != "*" check at Lines 81-85, which could become validation.When(r.MachineID != "*", validationis.UUID.Error(...)) inside ValidateStruct for a single-pass validation.

Also applies to: 72-99, 231-252

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rest-api/api/pkg/api/model/measurementtrust.go` around lines 17 - 20,
Refactor approvalType in the measurement-trust model into a named type with
receiver methods for proto conversion, replacing
validateMeasurementTrustApprovalType and the free conversion helpers. Update
ValidateStruct to validate ApprovalType with validation.Required and
validation.In using the two approval constants, and express the MachineID
wildcard exception with validation.When and the UUID rule in the same validation
pass.

Source: Coding guidelines

rest-api/openapi/spec.yaml (1)

1296-1329: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Missing pagination on the two new list endpoints.

Both get-all-measurement-trusted-machine and get-all-measurement-trusted-profile only accept siteId and omit pageNumber/pageSize/orderBy plus the X-Pagination response header, unlike essentially every other get-all-* endpoint in this spec — including other Site-scoped resources such as get-all-sku. Persist-type approvals have no forced expiry, so this list can grow unbounded at a busy Site, and callers have no way to page through results.

♻️ Suggested pagination addition (apply to both list operations)
       parameters:
         - schema:
             type: string
             format: uuid
           name: siteId
           in: query
           required: true
           description: ID of the Site
+        - schema:
+            type: integer
+            example: 1
+            default: 1
+            minimum: 1
+          in: query
+          name: pageNumber
+          description: Page number for pagination query
+        - schema:
+            type: integer
+            minimum: 1
+            maximum: 100
+            example: 20
+          in: query
+          name: pageSize
+          description: Page size for pagination query
       responses:
         '200':
           description: Measured Boot trusted Machine approvals
           content:
             application/json:
               schema:
                 type: array
                 items:
                   $ref: '`#/components/schemas/MeasurementTrustedMachine`'
+          headers:
+            X-Pagination:
+              schema:
+                type: string
+                example: '{"pageNumber":1,"pageSize":20,"total":30}'
+              description: Pagination result in JSON format

Also applies to: 1423-1456

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rest-api/openapi/spec.yaml` around lines 1296 - 1329, Update both list
operations, get-all-measurement-trusted-machine and
get-all-measurement-trusted-profile, to accept the standard pageNumber,
pageSize, and orderBy query parameters used by comparable get-all endpoints such
as get-all-sku. Add the X-Pagination response header to each 200 response while
preserving the existing siteId parameter and response schemas.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rest-api/api/pkg/api/model/measurementtrust.go`:
- Around line 126-180: Move the protobuf conversion logic from the free
functions APIMeasurementTrustedMachineFromProto and
APIMeasurementTrustedProfileFromProto onto FromProto receiver methods of their
respective API model structs. Replace the plural helpers
APIMeasurementTrustedMachinesFromProto and
APIMeasurementTrustedProfilesFromProto with named slice types and corresponding
FromProto receiver methods, preserving nil handling and element conversion
behavior.
- Around line 182-229: Replace MeasurementTrustedMachineRemoveProto and
MeasurementTrustedProfileRemoveProto with delete request DTOs containing
selector and path-derived ID fields, plus Validate and error-free ToProto
methods matching the existing create-request pattern. Populate these DTOs in the
handlers, run authorization before validation/conversion, then call Validate
followed by ToProto; preserve wildcard machine-ID handling and selector-specific
protobuf mapping.

---

Nitpick comments:
In `@rest-api/api/pkg/api/model/measurementtrust.go`:
- Around line 17-20: Refactor approvalType in the measurement-trust model into a
named type with receiver methods for proto conversion, replacing
validateMeasurementTrustApprovalType and the free conversion helpers. Update
ValidateStruct to validate ApprovalType with validation.Required and
validation.In using the two approval constants, and express the MachineID
wildcard exception with validation.When and the UUID rule in the same validation
pass.

In `@rest-api/openapi/spec.yaml`:
- Around line 1296-1329: Update both list operations,
get-all-measurement-trusted-machine and get-all-measurement-trusted-profile, to
accept the standard pageNumber, pageSize, and orderBy query parameters used by
comparable get-all endpoints such as get-all-sku. Add the X-Pagination response
header to each 200 response while preserving the existing siteId parameter and
response schemas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c9957257-ea84-4d0f-b9bf-31302c09ffd4

📥 Commits

Reviewing files that changed from the base of the PR and between 69cd51c and cc4c71c.

⛔ Files ignored due to path filters (7)
  • rest-api/sdk/standard/api_measured_boot_trusted_machine.go is excluded by !rest-api/sdk/standard/api_*.go
  • rest-api/sdk/standard/api_measured_boot_trusted_profile.go is excluded by !rest-api/sdk/standard/api_*.go
  • rest-api/sdk/standard/client.go is excluded by !rest-api/sdk/standard/client.go
  • rest-api/sdk/standard/model_measurement_trusted_machine.go is excluded by !rest-api/sdk/standard/model_*.go
  • rest-api/sdk/standard/model_measurement_trusted_machine_create_request.go is excluded by !rest-api/sdk/standard/model_*.go
  • rest-api/sdk/standard/model_measurement_trusted_profile.go is excluded by !rest-api/sdk/standard/model_*.go
  • rest-api/sdk/standard/model_measurement_trusted_profile_create_request.go is excluded by !rest-api/sdk/standard/model_*.go
📒 Files selected for processing (7)
  • rest-api/api/pkg/api/handler/measurementtrust.go
  • rest-api/api/pkg/api/handler/measurementtrust_test.go
  • rest-api/api/pkg/api/model/measurementtrust.go
  • rest-api/api/pkg/api/model/measurementtrust_test.go
  • rest-api/api/pkg/api/routes.go
  • rest-api/api/pkg/api/routes_test.go
  • rest-api/openapi/spec.yaml

Comment thread rest-api/api/pkg/api/model/measurementtrust.go Outdated
Comment thread rest-api/api/pkg/api/model/measurementtrust.go Outdated
Signed-off-by: Kyle Felter <kfelter@nvidia.com>
Signed-off-by: Kyle Felter <kfelter@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rest-api/api/pkg/api/model/measurementtrust.go (1)

85-99: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use named validation.By validators for the cross-field ID rules.
MachineID != "*" and the matching delete-path selector logic should live in receiver methods, then be composed through validation.Field(..., validation.By(...)) instead of inline branching. That keeps the validation reusable and consistent with the rest of the model package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rest-api/api/pkg/api/model/measurementtrust.go` around lines 85 - 99,
Refactor APIMeasurementTrustedMachineCreateRequest.Validate to move the
MachineID wildcard/UUID rule into a named receiver validator method and apply it
through validation.Field with validation.By. Apply the same pattern to the
matching delete-path selector logic, preserving the existing "*" allowance and
UUID validation behavior while removing inline branching.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rest-api/api/pkg/api/model/measurementtrust.go`:
- Around line 85-99: Refactor APIMeasurementTrustedMachineCreateRequest.Validate
to move the MachineID wildcard/UUID rule into a named receiver validator method
and apply it through validation.Field with validation.By. Apply the same pattern
to the matching delete-path selector logic, preserving the existing "*"
allowance and UUID validation behavior while removing inline branching.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b876a0c1-d3a2-4ebe-8d70-719e0c2c86f2

📥 Commits

Reviewing files that changed from the base of the PR and between d886233 and 6e0eee3.

📒 Files selected for processing (4)
  • rest-api/api/pkg/api/handler/measurementtrust.go
  • rest-api/api/pkg/api/handler/measurementtrust_test.go
  • rest-api/api/pkg/api/model/measurementtrust.go
  • rest-api/api/pkg/api/model/measurementtrust_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • rest-api/api/pkg/api/handler/measurementtrust_test.go
  • rest-api/api/pkg/api/handler/measurementtrust.go

@ajf

ajf commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

@kfelternv same comments about PR description, far too verbose.

@kfelternv

kfelternv commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

What changed

This PR adds Provider Admin REST and generated Go SDK operations to create, list, and delete measured-boot machine and profile trust approvals. Verification covers all six operations, both delete selectors for each resource, both approval types, wildcard machine approval, the existing-profile requirement, authorization, and request validation at revision 72f8f677ac2ddf83b679a5deb9bc6df7864c3cc8.

Scenario and setup

The NICo DevSpace stack is purged and redeployed from the exact PR revision. A Provider Admin performs the changed operations; a Tenant Admin checks the authorization boundary. The approval tables start empty.

Profile approvals require a measured-boot system profile, but this PR does not add system-profile REST CRUD. A uniquely named prerequisite profile is therefore created with nico-admin-cli before verification and removed with the same setup interface afterward. The admin CLI is not used to create, list, or delete trust approvals. Live database access is SELECT only.

Run the command blocks in Bash. Bearer-token values are sanitized as environment variables; the remaining setup, REST, SDK, and cleanup commands are directly pasteable.

Setup command:

devspace purge -n nico-system
devspace deploy --skip-build -n nico-system

RUN_DIR="$HOME/Developer/_agent-tmp/pr3464-verification"
mkdir -p "$RUN_DIR"
kubectl -n nico-rest port-forward service/nico-rest-api 18388:8388 >"$RUN_DIR/api-port-forward.log" 2>&1 &
API_PORT_FORWARD_PID=$!
kubectl -n nico-rest port-forward service/keycloak 18082:8082 >"$RUN_DIR/keycloak-port-forward.log" 2>&1 &
KEYCLOAK_PORT_FORWARD_PID=$!

export REST_BASE=http://localhost:18388
export API="$REST_BASE/v2/org/test-org/nico/measured-boot"
export PROVIDER_TOKEN="<Provider Admin bearer token>"
export TENANT_TOKEN="<Tenant Admin bearer token>"
export PROFILE_NAME=pr3464-template-20260722-213740
export MISSING_PROFILE_ID="$(python3 -c 'import uuid; print(uuid.uuid4())')"

export SITE_ID="$(
  curl -fsS --retry 10 --retry-connrefused --retry-delay 1 \
    -H "Authorization: Bearer $PROVIDER_TOKEN" \
    "$REST_BASE/v2/org/test-org/nico/site" |
    jq -er 'if type == "array" then .[0].id // .[0].siteId else .items[0].id // .items[0].siteId end'
)"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine?siteId=$SITE_ID"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile?siteId=$SITE_ID"
kubectl -n postgres exec postgres-0 -- psql -U postgres -d nico -Atc \
  'SELECT count(*) FROM measurement_approved_machines;'
kubectl -n postgres exec postgres-0 -- psql -U postgres -d nico -Atc \
  'SELECT count(*) FROM measurement_approved_profiles;'
PROFILE_CREATE="$(
  kubectl -n nico-system exec deployment/nico-api -- \
    /opt/carbide/nico-admin-cli \
    -a https://nico-api.nico-system.svc.cluster.local:1079 \
    -f json \
    attestation measured-boot profile create "$PROFILE_NAME" nvidia pr3464-template
)"
printf '%s\n' "$PROFILE_CREATE"
export PROFILE_ID="$(jq -r '.profile_id' <<<"$PROFILE_CREATE")"
kubectl -n postgres exec postgres-0 -- psql -U postgres -d nico -Atc \
  "SELECT count(*) FROM measurement_system_profiles WHERE name = '$PROFILE_NAME';"

Setup result:

Using namespace 'nico-system'
Using kube context 'kind-kind'
Reset complete; deploy with devspace deploy --skip-build
DEPLOY_HEAD=72f8f677ac2ddf83b679a5deb9bc6df7864c3cc8
Loaded REST images tagged 72f8f677-1784753577
REST API reports $SITE_ID online with all 2 Core hosts Ready and synced from a current inventory
REST integration setup complete
Initial API readiness attempt: connection refused; curl retry then succeeded
BASELINE_MACHINE=200 []
BASELINE_PROFILE=200 []
BASELINE_DB_MACHINE_COUNT=0
BASELINE_DB_PROFILE_COUNT=0
PROFILE_CREATE={"profile_id":"$PROFILE_ID","name":"pr3464-template-20260722-213740","ts":"2026-07-23T02:52:30.636704Z","attrs":[{"key":"product_name","value":"pr3464-template"},{"key":"sys_vendor","value":"nvidia"}]}
SETUP_PROFILE_COUNT=1

Verification

Step 1: Machine approval REST lifecycle

Why this step exists: It exercises machine POST, GET, and DELETE, including wildcard targets, both approval types, both delete selectors, response-field preservation, and not-found behavior.

Command or action:

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"machineId\":\"*\",\"approvalType\":\"Persist\",\"pcrRegisters\":\"0,2,7\",\"comments\":\"pr3464-machine-persist\"}" \
  "$API/trusted-machine"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine?siteId=$SITE_ID"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine/%2A?siteId=$SITE_ID&selector=MachineId"

curl -sS -o "$RUN_DIR/m2-post.json" -w 'HTTP_STATUS=%{http_code}\n' \
  -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"machineId\":\"*\",\"approvalType\":\"Oneshot\",\"pcrRegisters\":\"1,4\",\"comments\":\"pr3464-machine-oneshot\"}" \
  "$API/trusted-machine"
jq -c . "$RUN_DIR/m2-post.json"
export MACHINE_APPROVAL_ID_2="$(jq -r '.approvalId' "$RUN_DIR/m2-post.json")"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine/$MACHINE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ApprovalId"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine/$MACHINE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ApprovalId"

Observed result:

M1_POST=201 {"approvalId":"$MACHINE_APPROVAL_ID_1","machineId":"*","approvalType":"Persist","pcrRegisters":"0,2,7","comments":"pr3464-machine-persist","created":"2026-07-23T02:53:24.5506Z"}
M1_GET=200 [{"approvalId":"$MACHINE_APPROVAL_ID_1","machineId":"*","approvalType":"Persist","pcrRegisters":"0,2,7","comments":"pr3464-machine-persist","created":"2026-07-23T02:53:24.5506Z"}]
M1_DELETE_MACHINE_ID=200 {"approvalId":"$MACHINE_APPROVAL_ID_1","machineId":"*","approvalType":"Persist","pcrRegisters":"0,2,7","comments":"pr3464-machine-persist","created":"2026-07-23T02:53:24.5506Z"}
M2_POST=201 {"approvalId":"$MACHINE_APPROVAL_ID_2","machineId":"*","approvalType":"Oneshot","pcrRegisters":"1,4","comments":"pr3464-machine-oneshot","created":"2026-07-23T02:53:24.772978Z"}
M2_DELETE_APPROVAL_ID=200 {"approvalId":"$MACHINE_APPROVAL_ID_2","machineId":"*","approvalType":"Oneshot","pcrRegisters":"1,4","comments":"pr3464-machine-oneshot","created":"2026-07-23T02:53:24.772978Z"}
M2_REPEAT_DELETE=404 {"source":"nico","message":"rpc error: code = NotFound desc = measurement trusted machine approval not found: $MACHINE_APPROVAL_ID_2 (type: Error, retryable: true)","data":null}
FINAL_DB_MACHINE_COUNT=0

Why this proves the behavior: The new REST routes created both approval types, GET returned the exact persisted record, both MachineId and ApprovalId selectors returned the removed records, repeating deletion surfaced 404, and the read-only final count was zero.

Step 2: Profile approval REST lifecycle

Why this step exists: It exercises profile POST, GET, and DELETE, both approval types, both delete selectors, response-field preservation, and the Core existing-profile guard.

Command or action:

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"profileId\":\"$PROFILE_ID\",\"approvalType\":\"Oneshot\",\"pcrRegisters\":\"0,7\",\"comments\":\"pr3464-profile-oneshot\"}" \
  "$API/trusted-profile"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile?siteId=$SITE_ID"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile/$PROFILE_ID?siteId=$SITE_ID&selector=ProfileId"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile/$PROFILE_ID?siteId=$SITE_ID&selector=ProfileId"

curl -sS -o "$RUN_DIR/p2-post.json" -w 'HTTP_STATUS=%{http_code}\n' \
  -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"profileId\":\"$PROFILE_ID\",\"approvalType\":\"Persist\",\"pcrRegisters\":\"2,4\",\"comments\":\"pr3464-profile-persist\"}" \
  "$API/trusted-profile"
jq -c . "$RUN_DIR/p2-post.json"
export PROFILE_APPROVAL_ID_2="$(jq -r '.approvalId' "$RUN_DIR/p2-post.json")"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile/$PROFILE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ApprovalId"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile/$PROFILE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ApprovalId"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"profileId\":\"$MISSING_PROFILE_ID\",\"approvalType\":\"Persist\"}" \
  "$API/trusted-profile"

Observed result:

P1_POST=201 {"approvalId":"$PROFILE_APPROVAL_ID_1","profileId":"$PROFILE_ID","approvalType":"Oneshot","pcrRegisters":"0,7","comments":"pr3464-profile-oneshot","created":"2026-07-23T02:54:36.780853Z"}
P1_GET=200 [{"approvalId":"$PROFILE_APPROVAL_ID_1","profileId":"$PROFILE_ID","approvalType":"Oneshot","pcrRegisters":"0,7","comments":"pr3464-profile-oneshot","created":"2026-07-23T02:54:36.780853Z"}]
P1_DELETE_PROFILE_ID=200 {"approvalId":"$PROFILE_APPROVAL_ID_1","profileId":"$PROFILE_ID","approvalType":"Oneshot","pcrRegisters":"0,7","comments":"pr3464-profile-oneshot","created":"2026-07-23T02:54:36.780853Z"}
P1_REPEAT_DELETE=404 {"source":"nico","message":"rpc error: code = NotFound desc = measurement trusted profile approval not found: $PROFILE_ID (type: Error, retryable: true)","data":null}
P2_POST=201 {"approvalId":"$PROFILE_APPROVAL_ID_2","profileId":"$PROFILE_ID","approvalType":"Persist","pcrRegisters":"2,4","comments":"pr3464-profile-persist","created":"2026-07-23T02:54:37.089996Z"}
P2_DELETE_APPROVAL_ID=200 {"approvalId":"$PROFILE_APPROVAL_ID_2","profileId":"$PROFILE_ID","approvalType":"Persist","pcrRegisters":"2,4","comments":"pr3464-profile-persist","created":"2026-07-23T02:54:37.089996Z"}
P2_REPEAT_DELETE=404 {"source":"nico","message":"rpc error: code = NotFound desc = measurement trusted profile approval not found: $PROFILE_APPROVAL_ID_2 (type: Error, retryable: true)","data":null}
MISSING_PROFILE_POST=404 {"source":"nico","message":"rpc error: code = NotFound desc = MeasurementSystemProfile not found: $MISSING_PROFILE_ID (type: Error, retryable: true)","data":null}
FINAL_DB_PROFILE_COUNT=0

Why this proves the behavior: The profile routes created both approval types for the setup profile, GET returned the exact persisted record, both ProfileId and ApprovalId selectors removed and returned the records, repeated deletion produced 404, and Core rejected a valid nonexistent profile without leaving an approval row.

Step 3: Authorization and validation

Why this step exists: Every new route is Provider Admin-only, and the new request models define approval-type, identifier, and resource-specific selector validation.

Command or action:

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X POST \
  -H "Authorization: Bearer $TENANT_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"machineId\":\"*\",\"approvalType\":\"Persist\"}" \
  "$API/trusted-machine"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $TENANT_TOKEN" \
  "$API/trusted-machine?siteId=$SITE_ID"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X DELETE \
  -H "Authorization: Bearer $TENANT_TOKEN" \
  "$API/trusted-machine/$MACHINE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ApprovalId"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X POST \
  -H "Authorization: Bearer $TENANT_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"profileId\":\"$PROFILE_ID\",\"approvalType\":\"Persist\"}" \
  "$API/trusted-profile"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $TENANT_TOKEN" \
  "$API/trusted-profile?siteId=$SITE_ID"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X DELETE \
  -H "Authorization: Bearer $TENANT_TOKEN" \
  "$API/trusted-profile/$PROFILE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ApprovalId"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"machineId\":\"*\",\"approvalType\":\"Forever\"}" \
  "$API/trusted-machine"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"profileId\":\"$PROFILE_ID\",\"approvalType\":\"Forever\"}" \
  "$API/trusted-profile"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"machineId\":\"not-a-uuid\",\"approvalType\":\"Persist\"}" \
  "$API/trusted-machine"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X POST \
  -H "Authorization: Bearer $PROVIDER_TOKEN" -H 'Content-Type: application/json' \
  --data "{\"siteId\":\"$SITE_ID\",\"profileId\":\"not-a-uuid\",\"approvalType\":\"Persist\"}" \
  "$API/trusted-profile"

curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine/$MACHINE_APPROVAL_ID_2?siteId=$SITE_ID&selector=ProfileId"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' -X DELETE \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile/$PROFILE_APPROVAL_ID_2?siteId=$SITE_ID&selector=MachineId"

Observed result:

TENANT_STATUSES=machine-post:403,machine-get:403,machine-delete:403,profile-post:403,profile-get:403,profile-delete:403
TENANT_BODY_ALL_SIX={"source":"nico","message":"User does not have Provider Admin role with org","data":null}
invalid-machine-type=400 {"source":"nico","message":"invalid approvalType \"Forever\" (expected \"Oneshot\" or \"Persist\")","data":null}
invalid-profile-type=400 {"source":"nico","message":"invalid approvalType \"Forever\" (expected \"Oneshot\" or \"Persist\")","data":null}
invalid-machine-id=400 {"source":"nico","message":"machineId: must be a valid UUID","data":null}
invalid-profile-id=400 {"source":"nico","message":"profileId: must be a valid UUID.","data":null}
invalid-machine-selector=400 {"source":"nico","message":"Selector: invalid selector \"ProfileId\" (expected \"ApprovalId\" or \"MachineId\").","data":null}
invalid-profile-selector=400 {"source":"nico","message":"Selector: invalid selector \"MachineId\" (expected \"ApprovalId\" or \"ProfileId\").","data":null}
FINAL_DB_MACHINE_COUNT=0
FINAL_DB_PROFILE_COUNT=0

Why this proves the behavior: The Tenant Admin received the same Provider Admin authorization error from every new operation. The Provider Admin's malformed requests reached each new validation branch and returned the expected resource-specific 400 response without creating state.

Step 4: Generated Go SDK lifecycle

Why this step exists: The generated SDK is a separately changed public interface and must be exercised as a real consumer, not inferred from raw REST success.

Command or action:

export API_BASE_URL=http://localhost:18388 SITE_ID PROFILE_ID PROVIDER_TOKEN
docker run --rm --network host \
  --volume "$PR_CHECKOUT:$PR_CHECKOUT:ro" \
  --volume "$SDK_CONSUMER:$SDK_CONSUMER" \
  --workdir "$SDK_CONSUMER" \
  --env API_BASE_URL \
  --env SITE_ID --env PROFILE_ID --env PROVIDER_TOKEN \
  golang:1.26.4 go run -mod=mod .

Observed result:

SDK_MACHINE_CREATE=201 {"approvalId":"$SDK_MACHINE_APPROVAL_ID","approvalType":"Persist","comments":"pr3464-template-sdk-machine","created":"2026-07-23T03:01:53.275045Z","machineId":"*","pcrRegisters":"0,2,7"}
SDK_MACHINE_LIST=200 [{"approvalId":"$SDK_MACHINE_APPROVAL_ID","approvalType":"Persist","comments":"pr3464-template-sdk-machine","created":"2026-07-23T03:01:53.275045Z","machineId":"*","pcrRegisters":"0,2,7"}]
SDK_MACHINE_DELETE=200 {"approvalId":"$SDK_MACHINE_APPROVAL_ID","approvalType":"Persist","comments":"pr3464-template-sdk-machine","created":"2026-07-23T03:01:53.275045Z","machineId":"*","pcrRegisters":"0,2,7"}
SDK_PROFILE_CREATE=201 {"approvalId":"$SDK_PROFILE_APPROVAL_ID","approvalType":"Persist","comments":"pr3464-template-sdk-profile","created":"2026-07-23T03:01:53.776169Z","pcrRegisters":"0,2,7","profileId":"$PROFILE_ID"}
SDK_PROFILE_LIST=200 [{"approvalId":"$SDK_PROFILE_APPROVAL_ID","approvalType":"Persist","comments":"pr3464-template-sdk-profile","created":"2026-07-23T03:01:53.776169Z","pcrRegisters":"0,2,7","profileId":"$PROFILE_ID"}]
SDK_PROFILE_DELETE=200 {"approvalId":"$SDK_PROFILE_APPROVAL_ID","approvalType":"Persist","comments":"pr3464-template-sdk-profile","created":"2026-07-23T03:01:53.776169Z","pcrRegisters":"0,2,7","profileId":"$PROFILE_ID"}
SDK_FINAL_MACHINE_LIST=[]
SDK_FINAL_PROFILE_LIST=[]
SDK_ASSERTIONS=PASS

Why this proves the behavior: A standalone consumer imported the generated SDK from the tested revision. Its machine and profile service methods created, listed, and deleted real approvals through the deployed REST API, preserved every supplied field, and left both lists empty.

Cleanup action and result:

kubectl -n nico-system exec deployment/nico-api -- /opt/carbide/nico-admin-cli -a https://nico-api.nico-system.svc.cluster.local:1079 -f json attestation measured-boot profile delete "$PROFILE_ID" --is-id
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-machine?siteId=$SITE_ID"
curl -sS -w '\nHTTP_STATUS=%{http_code}\n' \
  -H "Authorization: Bearer $PROVIDER_TOKEN" \
  "$API/trusted-profile?siteId=$SITE_ID"
kubectl -n postgres exec postgres-0 -- psql -U postgres -d nico -Atc \
  "SELECT count(*) FROM measurement_system_profiles WHERE name = '$PROFILE_NAME';"
kubectl -n postgres exec postgres-0 -- psql -U postgres -d nico -Atc \
  'SELECT count(*) FROM measurement_approved_machines;'
kubectl -n postgres exec postgres-0 -- psql -U postgres -d nico -Atc \
  'SELECT count(*) FROM measurement_approved_profiles;'
kill "$API_PORT_FORWARD_PID" "$KEYCLOAK_PORT_FORWARD_PID"

PROFILE_DELETE={"profile_id":"$PROFILE_ID","name":"pr3464-template-20260722-213740","ts":"2026-07-23T02:52:30.636704Z","attrs":[{"key":"product_name","value":"pr3464-template"},{"key":"sys_vendor","value":"nvidia"}]}
SETUP_PROFILE_COUNT=0
FINAL_MACHINE_LIST=200 []
FINAL_PROFILE_LIST=200 []
FINAL_DB_MACHINE_COUNT=0
FINAL_DB_PROFILE_COUNT=0
DEPLOYED_REST_IMAGE=localhost:5000/nico-rest-api:72f8f677-1784753577
NICO_REST_RUNNING=8 NOT_READY=0 RESTARTS=0
NICO_SYSTEM_RUNNING=6 NOT_READY=0 RESTARTS=0
API_PORT_FORWARD=stopped
KEYCLOAK_PORT_FORWARD=stopped

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/api-core/src/measured_boot/tests/rpc.rs (1)

1644-1741: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solid NotFound coverage; minor DRY opportunity on the repeated assertions.

The four removal scenarios each repeat .await.unwrap_err(); assert_eq!(x.code(), tonic::Code::NotFound);. A tiny local helper would trim the boilerplate, though this is optional given each case calls a distinctly-typed handler/request.

♻️ Optional helper to reduce repetition
+fn assert_not_found<T>(result: Result<T, tonic::Status>) {
+    assert_eq!(result.unwrap_err().code(), tonic::Code::NotFound);
+}
+
 #[crate::sqlx_test]
 async fn test_remove_measurement_trust_approvals(

Then each of the four call sites becomes e.g. assert_not_found(site::handle_remove_measurement_trusted_machine(api, ...).await);.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/api-core/src/measured_boot/tests/rpc.rs` around lines 1644 - 1741,
Optionally reduce repetition in test_remove_measurement_trust_approvals by
adding a local helper that accepts the handler result and asserts
tonic::Code::NotFound, then reuse it for the four missing-machine and
missing-profile removal scenarios while preserving their distinct typed requests
and handlers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/api-core/src/measured_boot/tests/rpc.rs`:
- Around line 1644-1741: Optionally reduce repetition in
test_remove_measurement_trust_approvals by adding a local helper that accepts
the handler result and asserts tonic::Code::NotFound, then reuse it for the four
missing-machine and missing-profile removal scenarios while preserving their
distinct typed requests and handlers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f9bece7a-7b1a-4a69-834b-7aaf46411582

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0eee3 and 07489f7.

📒 Files selected for processing (3)
  • crates/api-core/src/measured_boot/rpc/site.rs
  • crates/api-core/src/measured_boot/tests/rpc.rs
  • crates/api-db/src/measured_boot/interface/site.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants